package com.foxchen.offline.task;


import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.foxchen.offline.task.bean.TaskPojo;
import com.foxchen.offline.task.config.OfflineTaskConfig;
import com.foxchen.offline.task.enums.OfflineTaskStatusEnum;
import com.foxchen.offline.task.service.OfflineTaskService;
import com.foxchen.offline.task.util.FileUtils;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.BeanUtils;

import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author 6774000316@zte.intra
 */
@Slf4j
public abstract class ExportHandler {
    private static final Integer PROGRESS_OVER = 100;

    private static final Integer PROGRESS_WAIT_OVER = 99;

    private static final Integer PROGRESS_SAVE = 2;

    private static final ThreadLocal<TaskPojo> TASK_POJO_THREAD_LOCAL = new ThreadLocal<>();

    private static final String FILE_TYPE_ZIP = ".zip";

    public Boolean dealExport(OfflineTaskConfig exportConfig,
                              TaskPojo exportTaskBean,
                              Map<String, Future<Boolean>> runMap,
                              OfflineTaskService offlineTaskService) {
        try {
            // 放入任务信息
            TASK_POJO_THREAD_LOCAL.set(exportTaskBean);
            log.info("ExportHandler begin export,exportConfig={},task={},runMap size={}",exportConfig,getTaskInfo(),runMap.size());

            // 更新任务为进行中
            exportTaskBean.setStatus(OfflineTaskStatusEnum.GOING.getStatus());
            exportTaskBean.setGmtModified(new Date());
            offlineTaskService.updateTask(exportTaskBean);
            // 开始导出
            asyncExportByType(exportConfig,offlineTaskService);
            log.info("ExportHandler dealExport success.");
            return Boolean.TRUE;
        }
        // 执行失败，更新任务状态
        catch (Throwable e) {
            log.error("ExportHandler dealExport error.",e);
            exportTaskBean.setStatus(OfflineTaskStatusEnum.FAILED.getStatus());
            exportTaskBean.setGmtModified(new Date());
            offlineTaskService.updateTask(exportTaskBean);
            return Boolean.FALSE;
        }
        // 从执行中的map移除当前任务
        finally {
            log.info("ExportHandler deal export over.");
            runMap.remove(exportTaskBean.getId());
            TASK_POJO_THREAD_LOCAL.remove();
        }
    }

    private void asyncExportByType(OfflineTaskConfig exportConfig,OfflineTaskService offlineTaskService) throws Exception {
        // 导出csv
        if(ExcelTypeEnum.CSV.getValue().equals(getTaskInfo().getFileSuffix())){
            log.info("begin export by csv.");
            directExport(ExcelTypeEnum.CSV,exportConfig,offlineTaskService);
        }

        // 如果是导出xlsx文件，继续判断每个sheet的数据量，任意一个sheet数据超过配置的数据量，那么此次导出需要进行拆分excel文件最终导出zip
        if(ExcelTypeEnum.XLSX.getValue().equals(getTaskInfo().getFileSuffix())){
            boolean isZip = false;
            for (WriteSheet writeSheet : getSheetList()) {
                Pair<Long, Long> totalAndSingleBatch = getTotalAndSingleBatch(writeSheet.getSheetNo());
                if(totalAndSingleBatch.getLeft() > exportConfig.getSingleSheetMaxLine()){
                    log.info("sheetNo={},total={},more than config,need export zip.",writeSheet.getSheetNo(),totalAndSingleBatch.getLeft());
                    isZip = true;
                    break;
                }
            }
            if(isZip){
                log.info("begin export by zip.");
                exportZip(ExcelTypeEnum.XLSX,exportConfig,offlineTaskService);
            }
            else {
                log.info("begin export by xlsx.");
                directExport(ExcelTypeEnum.XLSX,exportConfig,offlineTaskService);
            }
        }
    }

    /**
     * 默认的zip数据导出逻辑
     * 如果想要自己导出逻辑，可以重写此方法
     */
    public void exportZip(ExcelTypeEnum excelTypeEnum,
                          OfflineTaskConfig exportConfig,
                          OfflineTaskService offlineTaskService) throws Exception {
        // 根据配置的模块与上传文件的目录，创建文件
        String fileId = UUID.randomUUID().toString();
        String moduleDateDir = FileUtils.getDateFileDir(exportConfig.getModuleName());
        String moduleUuidPath = FileUtils.getDateFileName(moduleDateDir, fileId, FILE_TYPE_ZIP);
        String fileRealPath = exportConfig.getExportPath() + File.separator + moduleUuidPath;
        File file = FileUtils.checkAndCreateFile(fileRealPath);

        try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(Files.newOutputStream(Paths.get(fileRealPath)));
             ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(bufferedOutputStream)) {
            zipOut.setEncoding("GBK");
            // 获取所有的sheet
            List<WriteSheet> allSheetList = getSheetList();
            log.info("exportZip sheet size={},allSheetList={}",allSheetList.size(),allSheetList);
            AtomicInteger excelFileNum = new AtomicInteger(BigDecimal.ZERO.intValue());
            for (int i = 0; i < allSheetList.size(); i++) {
                WriteSheet sheet = allSheetList.get(i);

                // 当前sheet数据总数以及每一批写入多少条
                Pair<Long, Long> totalAndSingleBatch = getTotalAndSingleBatch(sheet.getSheetNo());

                // 有数据才需要写入
                if(totalAndSingleBatch.getLeft() > BigDecimal.ZERO.intValue()){
                    // 算出总共的页数
                    long pageSize = totalAndSingleBatch.getRight();
                    long totalPage = getTotalPageByTotalNumAndPageNum(totalAndSingleBatch);
                    log.info("sheetNum={},sheetName={},sheetHeader={},sheetClass={},sheetTotal={},sheetPageSize={},totalPage={}",
                            sheet.getSheetNo(),sheet.getSheetName(),sheet.getHead(),sheet.getClazz(),totalAndSingleBatch.getLeft(),pageSize,totalPage);


                    // 先放一个excel文件
                    int excelFileNumber = excelFileNum.addAndGet(BigDecimal.ONE.intValue());
                    putNextEntry(excelFileNumber, zipOut);

                    // 往excel文件一页一页的写数据
                    pageWriteZip(excelTypeEnum,sheet,Pair.of(pageSize,totalPage),exportConfig,zipOut,excelFileNum,allSheetList,offlineTaskService);
                }
            }

            zipOut.close();

            // 写完数据后
            updateTaskComplete(moduleUuidPath,fileId,offlineTaskService);
        } catch (Exception e){
            boolean delete = file.delete();
            log.error("something went wrong when directExport,is file delete?:{},filePath:{}",delete,fileRealPath,e);
            throw e;
        }
    }

    private void pageWriteZip(ExcelTypeEnum excelTypeEnum,
                              WriteSheet sheet,
                              Pair<Long,Long> pageSizeAndTotalPage,
                              OfflineTaskConfig exportConfig,
                              ZipArchiveOutputStream zipOut,
                              AtomicInteger excelFileNum,
                              List<WriteSheet> allSheetList,
                              OfflineTaskService offlineTaskService) throws Exception {
        // 从第一页开始写数据
        long pageNo = BigDecimal.ONE.longValue();

        Long pageSize = pageSizeAndTotalPage.getLeft();
        Long totalPage = pageSizeAndTotalPage.getRight();

        // 标记当前excel文件总共已经拉到了多少数据
        AtomicLong localExcelAlreadyNum = new AtomicLong(BigDecimal.ZERO.intValue());

        // 构建writer 写入基础数据
        ExcelWriter writer = buildWriter(zipOut,excelTypeEnum);
        writeBaseSheet(writer,allSheetList,excelTypeEnum);

        while (pageNo <= totalPage) {
            // 获取当前页数据，没有则继续
            List<?> singleBatchData = pageGetSheetData(sheet.getSheetNo(), pageNo, pageSize);
            log.debug("sheetNum={},pageNo={},data={}",sheet.getSheetNo(),pageNo,singleBatchData);
            if(CollectionUtils.isEmpty(singleBatchData)){
                log.warn("sheetNo:{} page:{} not collect data.",sheet.getSheetNo(),pageNo);
                pageNo += 1;
                continue;
            }

            // 如果当前总的拉到的数据小于配置的单个excel文件，那么可以直接写当前这批数据
            long localExcelAlreadyNumber = localExcelAlreadyNum.addAndGet(singleBatchData.size());
            if(localExcelAlreadyNumber <= exportConfig.getSingleSheetMaxLine()){
                writeDataByType(writer,singleBatchData,sheet,excelTypeEnum);
            }
            // 否则如果当前拉到的数据大于配置单个excel文件时，需要另外处理
            else {
                // 超过前的数据写到之前的excel文件，关闭writer,关闭当前文件
                long moreSize = localExcelAlreadyNumber - exportConfig.getSingleSheetMaxLine();
                List<?> subData = singleBatchData.subList(0, (int) (singleBatchData.size() - moreSize));
                writeDataByType(writer,subData,sheet,excelTypeEnum);
                closeWriter(writer);
                zipOut.closeArchiveEntry();

                // 超出后的数据继续分excel写入
                List<?> nextSubData = singleBatchData.subList((int) (singleBatchData.size() - moreSize),singleBatchData.size());
                List<? extends List<?>> splitData = Lists.partition(nextSubData, exportConfig.getSingleSheetMaxLine());
                for (int i1 = 0; i1 < splitData.size(); i1++) {
                    List<?> singleExcelData = splitData.get(i1);

                    // 文件+1
                    int excelFileNumber = excelFileNum.addAndGet(BigDecimal.ONE.intValue());
                    putNextEntry(excelFileNumber,zipOut);

                    // 往当前文件写入数据
                    writer = buildWriter(zipOut,ExcelTypeEnum.XLSX);
                    writeBaseSheet(writer,allSheetList,excelTypeEnum);
                    writeDataByType(writer,singleExcelData,sheet,excelTypeEnum);

                    // 如果还能循环，那么证明当前的文件需要关闭
                    if(i1 != splitData.size() - 1){
                        closeWriter(writer);
                        zipOut.closeArchiveEntry();
                    }
                    else {
                        boolean isLastPage = pageNo == totalPage;
                        Pair<AtomicLong, AtomicInteger> numData = Pair.of(localExcelAlreadyNum, excelFileNum);
                        writer = cycleWriteDateWhenLast(isLastPage,excelTypeEnum,singleExcelData,
                                exportConfig.getSingleSheetMaxLine(),numData,writer,zipOut,allSheetList);
                    }
                }
            }

            // 更新任务进度
            Integer progress = getProgress(allSheetList.size(), sheet.getSheetNo(), pageNo, totalPage);
            log.debug("allSheetList={},sheetNum={},pageNo={},totalPage={},progress={}",
                    allSheetList.size(),sheet.getSheetNo(),pageNo,singleBatchData,progress);
            getTaskInfo().setProgress(progress);
            getTaskInfo().setGmtModified(new Date());
            offlineTaskService.updateTask(getTaskInfo());

            pageNo += 1;
        }

        // 关闭当前sheet最后的writer
        closeWriter(writer);
        // 关闭当前sheet的最后一个文件
        zipOut.closeArchiveEntry();
    }

    protected ExcelWriter cycleWriteDateWhenLast(boolean isLastPage,
                                               ExcelTypeEnum excelTypeEnum,
                                               List<?> singleExcelData,
                                               Integer singleSheetMaxLine,
                                               Pair<AtomicLong,AtomicInteger> numData,
                                               ExcelWriter writer,
                                               ZipArchiveOutputStream zipOut,
                                               List<WriteSheet> allSheetList) throws Exception{
        AtomicLong localExcelAlreadyNum = numData.getLeft();
        AtomicInteger excelFileNum = numData.getRight();

        // 如果写到最后，发现这个writer还能继续写，那么标记一下
        if(singleExcelData.size() < singleSheetMaxLine){
            localExcelAlreadyNum.set(singleExcelData.size());
        }
        // 如果写到最后，但是并不是最后一页，发现这个writer不能继续写，那么原write close，文件+1，writer重新赋值，已经写入的标记0
        else {
            if(!isLastPage){
                closeWriter(writer);
                zipOut.closeArchiveEntry();
                localExcelAlreadyNum.set(BigDecimal.ZERO.intValue());
                int excelFileNumber = excelFileNum.addAndGet(BigDecimal.ONE.intValue());
                putNextEntry(excelFileNumber,zipOut);
                writer = buildWriter(zipOut,ExcelTypeEnum.XLSX);
                writeBaseSheet(writer,allSheetList,excelTypeEnum);
            }
        }

        return writer;
    }

    private ExcelWriter buildWriter(OutputStream outputStream, ExcelTypeEnum excelTypeEnum){
        return EasyExcel
                .write(outputStream)
                .autoCloseStream(Boolean.FALSE)
                .excelType(excelTypeEnum)
                .charset(Charset.forName("GBK"))
                .build();
    }

    private void writeBaseSheet(ExcelWriter writer,List<WriteSheet> sheets,ExcelTypeEnum excelTypeEnum){
        for (WriteSheet writeSheet : sheets) {
            writeDataByType(writer,new ArrayList<>(),writeSheet,excelTypeEnum);
        }
    }
    
    private void closeWriter(ExcelWriter writer){
        writer.finish();
        writer.close();
    }

    private void putNextEntry(int fileNum, ZipArchiveOutputStream zipOut) throws IOException {
        String nextExcelName = getTaskInfo().getName() + "-" + fileNum + ExcelTypeEnum.XLSX.getValue();
        log.info("put xlsx name:{}",nextExcelName);
        zipOut.putArchiveEntry(new ZipArchiveEntry(nextExcelName));
    }

    private void directExport(ExcelTypeEnum excelType,OfflineTaskConfig exportConfig,OfflineTaskService offlineTaskService) throws Exception{
        // 根据配置的模块与上传文件的目录，创建文件
        String fileId = UUID.randomUUID().toString();
        String moduleDateDir = FileUtils.getDateFileDir(exportConfig.getModuleName());
        String moduleUuidPath = FileUtils.getDateFileName(moduleDateDir, fileId, excelType.getValue());
        String fileRealPath = exportConfig.getExportPath() + File.separator + moduleUuidPath;
        File file = FileUtils.checkAndCreateFile(fileRealPath);

        // 每个sheet循环写入writer
        try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(Files.newOutputStream(Paths.get(fileRealPath)));
             ExcelWriter writer = buildWriter(bufferedOutputStream,excelType)) {
            List<WriteSheet> allSheetList = getSheetList();
            writeBaseSheet(writer,allSheetList,excelType);
            log.info("export:{} size={},allSheetList={}",excelType.getValue(),allSheetList.size(),allSheetList);

            // 遍历每个sheet
            if(CollectionUtils.isNotEmpty(allSheetList)){
                for (WriteSheet sheet : allSheetList) {
                    // 获取当前sheet的总量以及每批导入多少数据
                    Pair<Long, Long> totalAndSingleBatch = getTotalAndSingleBatch(sheet.getSheetNo());
                    long pageNo = BigDecimal.ONE.longValue();
                    long pageSize = totalAndSingleBatch.getRight();
                    long totalPage = getTotalPageByTotalNumAndPageNum(totalAndSingleBatch);
                    log.info("sheetNum={},sheetName={},sheetHeader={},sheetClass={},sheetTotal={},sheetPageSize={},totalPage={}",
                            sheet.getSheetNo(),sheet.getSheetName(),sheet.getHead(),sheet.getClazz(),totalAndSingleBatch.getLeft(),pageSize,totalPage);

                    while (pageNo <= totalPage) {
                        // 获取当前页数据
                        List<?> singleBatchData = pageGetSheetData(sheet.getSheetNo(), pageNo, pageSize);
                        log.debug("sheetNum={},pageNo={},data={}",sheet.getSheetNo(),pageNo,singleBatchData);

                        writeDataByType(writer,singleBatchData,sheet,excelType);

                        // 更新任务进度
                        Integer progress = getProgress(allSheetList.size(), sheet.getSheetNo(), pageNo, totalPage);
                        log.debug("allSheetList={},sheetNum={},pageNo={},totalPage={},progress={}",
                                allSheetList.size(),sheet.getSheetNo(),pageNo,singleBatchData,progress);
                        getTaskInfo().setProgress(progress);
                        getTaskInfo().setGmtModified(new Date());
                        offlineTaskService.updateTask(getTaskInfo());

                        pageNo += 1;
                    }
                }

                // 所有sheet写入完后，finish
                closeWriter(writer);
            }

            // 写完数据后，更新任务状态为完成
            updateTaskComplete(moduleUuidPath,fileId,offlineTaskService);
        } catch (Exception e){
            boolean delete = file.delete();
            log.error("something went wrong when directExport,is file delete?:{},filePath:{}",delete,fileRealPath,e);
            throw e;
        }
    }

    private void writeDataByType(ExcelWriter writer,List<?> singleBatchData,WriteSheet sheet,ExcelTypeEnum excelType){
        // xlsx直接导出
        if(ExcelTypeEnum.XLSX.equals(excelType)){
            writer.write(singleBatchData,sheet);
        }
        // csv只能导出到同一个sheetNo里面
        else if(ExcelTypeEnum.CSV.equals(excelType)){
            WriteSheet writeSheet = new WriteSheet();
            BeanUtils.copyProperties(sheet,writeSheet);
            writeSheet.setSheetNo(BigDecimal.ONE.intValue());
            writer.write(singleBatchData,writeSheet);
        }
    }

    private void updateTaskComplete(String dateFileName, String fileId, OfflineTaskService offlineTaskService) {
        TaskPojo taskInfo = getTaskInfo();
        taskInfo.setStatus(OfflineTaskStatusEnum.SUCCESS.getStatus());
        taskInfo.setProgress(PROGRESS_OVER);
        taskInfo.setGmtModified(new Date());
        taskInfo.setFilePath(dateFileName);
        taskInfo.setFileId(fileId);
        taskInfo.setGmtModified(new Date());
        offlineTaskService.updateTask(taskInfo);
    }


    /**
     * 计算任务进度
     * @param allSheetNum 总sheet数
     * @param localSheetNum 当前sheet
     * @param pageNo 当前查询页数
     * @param queryCount 总查询次数
     * @return 进度
     */
    private Integer getProgress(Integer allSheetNum,Integer localSheetNum,Long pageNo,Long queryCount) {
        // 如果是最后一个sheet页的最后一次处理，任务进度99
        if(Objects.equals(allSheetNum, localSheetNum) && Objects.equals(pageNo, queryCount)){
            return PROGRESS_WAIT_OVER;
        }

        // 已经执行完了的进度
        BigDecimal basicProgress = BigDecimal
                .valueOf(localSheetNum - BigDecimal.ONE.intValue())
                .divide(BigDecimal.valueOf(allSheetNum), PROGRESS_SAVE, RoundingMode.HALF_UP);
        log.info("allSheetNum= {},sheetNum= {},basicProgress= {}",allSheetNum,localSheetNum,basicProgress);

        // 计算单个sheet进度比例 1/所有sheet页
        BigDecimal singleSheetProgress = BigDecimal.ONE
                .divide(BigDecimal.valueOf(allSheetNum), PROGRESS_SAVE, RoundingMode.HALF_UP);
        log.info("singleSheetProgress= {}",singleSheetProgress);

        // 单个sheet页进度比例 * (当前sheet页分页查询页数 / 当前sheet页分页查询总页数) = 当前sheet页进度
        BigDecimal queryProgress = BigDecimal
                .valueOf(pageNo)
                .divide(BigDecimal.valueOf(queryCount), PROGRESS_SAVE, RoundingMode.HALF_UP);
        BigDecimal localPageProgress = singleSheetProgress.multiply(queryProgress);
        log.info("localPageProgress= {}",localPageProgress);

        // (已经跑完了的进度 + 当前sheet页进度）* 100 = 总任务进度
        Integer localProgress = basicProgress
                .add(localPageProgress)
                .multiply(BigDecimal.valueOf(PROGRESS_OVER)).setScale(PROGRESS_SAVE,RoundingMode.HALF_UP)
                .intValue();
        log.info("====================export progress========================>{}",localProgress);

        return localProgress;
    }

    private long getTotalPageByTotalNumAndPageNum(Pair<Long, Long> totalAndSingleBatch) {
        long pageSize = totalAndSingleBatch.getRight();
        long totalData = totalAndSingleBatch.getLeft();
        long totalPage = totalData / pageSize;
        return totalData % pageSize > BigDecimal.ZERO.longValue() ? totalPage + BigDecimal.ONE.longValue() : totalPage;
    }

    /**
     * 获取当前执行的任务信息
     * @return 任务信息
     */
    public TaskPojo getTaskInfo(){
        return TASK_POJO_THREAD_LOCAL.get();
    }

    /**
     * 根据sheet页码获取total的方法以及每一个sheet每批导入多少条数据
     * @return Pair key:sheet页对应数据总量 value:该sheet页每批导多少条数据
     */
    public abstract Pair<Long,Long> getTotalAndSingleBatch(Integer sheetNum) throws Exception;

    /**
     * 组装sheet页方法（需要自己组装好表头）
     * @return sheet页，包括sheet页码及名称
     */
    public abstract List<WriteSheet> getSheetList() throws Exception;

    /**
     * 获取数据的方法
     * @param sheetNo  sheet页编号
     * @param pageNo   当前页码
     * @param pageSize 每页大小
     * @return 表体数据 可以是bean 也可以是双层List
     */
    public abstract List<?> pageGetSheetData(Integer sheetNo, Long pageNo, Long pageSize) throws Exception;
}
